﻿using System;
using System.Reflection;
using Fabrication.Exceptions;

namespace Fabrication
{
    public class LinkDetails : ILinkDetails
    {
        private readonly Func<object, object> sourceValueModifier;
        private readonly SourceValue sourceValue = new SourceValue();
        private readonly Type sourceOwningType;
        private readonly Type sourcePropertyType;
        private readonly string sourcePropertyName;

        public LinkDetails(Type sourceOwningType, Type targetOwningType, Type propertyType, string sourcePropertyName, string targetPropertyName)
            : this(sourceOwningType, targetOwningType, propertyType, sourcePropertyName, propertyType, targetPropertyName, x => x) { }

        public LinkDetails(Type sourceOwningType, Type targetOwningType, Type sourcePropertyType, string sourcePropertyName, Type targetPropertyType, string targetPropertyName)
            : this(sourceOwningType, targetOwningType, sourcePropertyType, sourcePropertyName, targetPropertyType, targetPropertyName, x => x) { }

        public LinkDetails(Type sourceOwningType, Type targetOwningType, Type propertyType, string sourcePropertyName, string targetPropertyName, Func<object, object> sourceValueModifier)
            : this(sourceOwningType, targetOwningType, propertyType, sourcePropertyName, propertyType, targetPropertyName, sourceValueModifier) { }

        public LinkDetails(Type sourceOwningType, Type targetOwningType, Type sourcePropertyType, string sourcePropertyName, Type targetPropertyType, string targetPropertyName, Func<object, object> sourceValueModifier)
        {
            if (sourceOwningType == null ||
                targetOwningType == null ||
                sourcePropertyType == null ||
                targetPropertyType == null ||
                String.IsNullOrWhiteSpace(sourcePropertyName) ||
                String.IsNullOrWhiteSpace(targetPropertyName))
                throw new LinkedPropertyException("All parameters are required for a valid link");

            this.sourceOwningType = sourceOwningType;
            this.sourcePropertyType = sourcePropertyType;
            this.sourcePropertyName = sourcePropertyName;
            TargetOwningType = targetOwningType;
            TargetPropertyType = targetPropertyType;
            TargetPropertyName = targetPropertyName;
            this.sourceValueModifier = sourceValueModifier;
        }

        public Type TargetOwningType { get; private set; }
        public Type TargetPropertyType { get; private set; }
        public string TargetPropertyName { get; private set; }
        public bool HasSourceValues { get { return sourceValue.IsSet; } }

        public void SetSourceValue(object value)
        {
            if (value != null)
            {
                var valueType = value.GetType();
                if ((typeof(Nullable<>).IsAssignableFrom(sourcePropertyType) && valueType != sourcePropertyType.GetGenericArguments()[0]) && valueType != sourcePropertyType)
                throw new LinkedPropertyException(string.Format("'{0}' must be assignable to '{1}' to be a valid source value.", value, sourcePropertyType.Name));
            }
            sourceValue.Value = value;
        }

        public object GetTargetValue()
        {
            if (!HasSourceValues)
                throw new LinkedPropertyException("Cannot generate target value before source value has been set.");

            return sourceValueModifier(sourceValue.Value);
        }

        public bool SourceMatches(PropertyInfo property)
        {
            return SourceMatches(property.Name, property.PropertyType, property.ReflectedType);
        }

        public bool SourceMatches(string propertyName, Type propertyType, Type owningType)
        {
            return sourceOwningType == owningType &&
                   sourcePropertyType == propertyType &&
                   sourcePropertyName == propertyName;
        }

        public bool TargetMatches(PropertyInfo targetProperty)
        {
            return TargetMatches(targetProperty.Name,
                targetProperty.PropertyType,
                targetProperty.ReflectedType);
        }

        public bool TargetMatches(ILinkDetails linkDetails)
        {
            return TargetMatches(linkDetails.TargetPropertyName,
                linkDetails.TargetPropertyType,
                linkDetails.TargetOwningType);
        }

        public bool TargetMatches(string propertyName, Type propertyType, Type owningType)
        {
            return TargetOwningType == owningType &&
                   TargetPropertyType == propertyType &&
                   TargetPropertyName == propertyName;
        }
    }
}